using System;
using System.Linq;
using Computing; 

namespace Neural
{
    /// <summary>
    /// Summary description for MLPerceptron.
    /// </summary>
    [Serializable]
    public class MLPerceptron : CellSet
    {
        #region Data structures
        protected Vector InputAverage; 
        protected Vector InputStddev; 

        protected Can _1_; 
        protected Can[] Input; 
        protected Can[] dL_dOutput; 
        protected Can[] Output; 
        protected Cell[][] Neuron; 

        protected Can[] Weight; 
        protected Can[]	dL_dWeight; 
        #endregion

        #region Construction and initialization
        public MLPerceptron()
        {
        }

        protected virtual Cell GetNewCell()
        {
            return new Cell(); 
        }

        private void FlattenWeights()
        {
            int layers_nr = Neuron.Length;

            int[] width = new int[layers_nr + 2];
            width[0] = InputAverage.Dimension;
            for (int i = 0; i < layers_nr; ++i)
            {
                width[i + 1] = Neuron[i].Length;
            }

            int weights_nr = 0;
            for (int i = 0; i < layers_nr; i++)
                for (int j = 0; j < width[i + 1]; j++)
                    weights_nr += width[i] + 1;

            Weight = new Can[weights_nr];
            dL_dWeight = new Can[weights_nr];

            int w = 0;
            for (int i = 0; i < layers_nr; i++)
                for (int j = 0; j < width[i + 1]; j++)
                    for (int k = 0; k < width[i] + 1; k++)
                    {
                        Weight[w] = Neuron[i][j].Weight[k];
                        dL_dWeight[w++] = Neuron[i][j].dL_dWeight[k];
                    }
        }

        public void Build(int in_dim, CellType[][] types)
        {
            #region checking correctenss of arguments
            int i, j, k;
            int layers_nr = types.GetLength(0); //	number of layers in the network 
            int out_dim = types[layers_nr-1].GetLength(0); 
            if (in_dim<0 || layers_nr<=0)
                throw (new System.Exception("Neural.PureStructure.Build: in_dim or layers_nr")); 
            for (i=0; i<layers_nr; i++)
                if (types[i].GetLength(0)<1)
                    throw (new System.Exception("Neural.PureStructure.Build: layer types no < 1")); 
            #endregion

            #region Initializing structures
            _1_ = new Can(); 
            _1_.Value = 1;
            InputAverage = new Vector(in_dim);             
            InputStddev = new Vector(in_dim); 
            Input = new Can[in_dim]; 
            for (i=0; i<in_dim; i++) 
            {
                InputAverage[i] = 0; 
                InputStddev[i] = 1;
                Input[i] = new Can(); 
            }

            Neuron = new Cell[layers_nr][]; 
            int[] width = new int[layers_nr+2];	//	numbers of neurons in layres
            width[0] = in_dim; 
            for (i=0; i<layers_nr; i++)
                width[i+1] = types[i].GetLength(0); 
            width[layers_nr+1] = 1; 
            for (i=0; i<layers_nr; i++)
            {
                Neuron[i] = new Cell[width[i+1]]; 
                for (j=0; j<width[i+1]; j++)
                {
                    Neuron[i][j] = GetNewCell(); 
                    Neuron[i][j].Init(width[i]+1, types[i][j], width[i+2]); 
                }
            }

            Output = new Can[out_dim];
            dL_dOutput = new Can[out_dim]; 
            for (i=0; i<out_dim; i++)
            {
                Output[i] = Neuron[layers_nr-1][i].Output; 
                dL_dOutput[i] = new Can(); 
            }
            #endregion

            #region Connecting neurons backward
            //	i=0
            if (true)
            {
                for (j=0; j<width[1]; j++)
                {
                    for (k=0; k<width[0]; k++)
                        Neuron[0][j].Input[k] = Input[k]; 
                    Neuron[0][j].Input[k] = _1_; 
                }
            }
            for (i=1; i<layers_nr; i++)
            {
                for (j=0; j<width[i+1]; j++)
                {
                    for (k=0; k<width[i]; k++)
                        Neuron[i][j].Input[k] = Neuron[i-1][k].Output; 
                    Neuron[i][j].Input[k] = _1_; 
                }
            }
            #endregion

            #region Connecting neurons forward
            for (i=0; i<layers_nr-1; i++)
            {
                for (j=0; j<width[i+1]; j++)
                {
                    for (k=0; k<width[i+2]; k++)
                    {
                        Neuron[i][j].dL_dnextX[k] = Neuron[i+1][k].dL_dX; 
                        Neuron[i][j].dnextX_dOutput[k] = Neuron[i+1][k].Weight[j]; 
                    }
                }
            }
            // i=layers_nr-1
            if (true)
            {
                for (j=0; j<width[i+1]; j++)
                {
                    for (k=0; k<width[i+2]; k++)
                    {
                        Neuron[i][j].dL_dnextX[k] = dL_dOutput[j]; 
                        Neuron[i][j].dnextX_dOutput[k] = _1_; 
                    }
                }
            }	
            #endregion

            FlattenWeights();

            AfterConstruction(); 
        }

        public void Build(int in_dim, CellType type, int[] width)
        {
            int i, j; 
            int layers_nr = width.GetLength(0);	//	number of layers in the network 
            if (in_dim<0 || layers_nr<1)
                throw (new System.Exception("Thrown at Neural.PureStructure.Build(int, CellType, int[])"));

            CellType[][] types = new CellType[layers_nr][]; 
            for (i=0; i<layers_nr; i++)
                types[i] = new CellType[width[i]]; 

            for (i=0; i<layers_nr-1; i++)
                for (j=0; j<width[i]; j++)
                    types[i][j] = type; 
            //	i = layers_nr-1
            for (j=0; j<width[i]; j++)
                types[i][j] = CellType.Linear; 

            Build(in_dim, types); 
        }

        virtual protected void AfterConstruction()
        {
        }

        public void SetInputDescription(Vector in_av, Vector in_stddev)
        {
            int i, in_dim = Input.GetLength(0); 
            if (in_av.Dimension!= in_dim
                || in_stddev.Dimension != in_dim)
                throw (new System.Exception("Thrown at Neural.MLPerceptron.SetInputDescription(Vector, Vector)")); 
            for (i=0; i<in_dim; i++)
                if (in_stddev[i]<=0)
                    throw (new System.Exception("Thrown at Neural.MLPerceptron.SetInputDescription(Vector, Vector)")); 

            for (i=0; i<in_dim; i++)
            {
                InputAverage[i] = in_av[i]; 
                InputStddev[i] = in_stddev[i]; 
            }
        }

        public void InitWeights(double std_dev)
        {
            int i, j, layers_nr = Neuron.GetLength(0); 
            for (i=0; i<layers_nr-1; i++)
                for (j=0; j<Neuron[i].GetLength(0); j++)
                    Neuron[i][j].InitWeights(TheSampler, std_dev); 
            //	i=layers_nr-1; 
            for (j=0; j<Neuron[i].GetLength(0); j++)
                Neuron[i][j].InitWeights(TheSampler, 0); 
        }

        public void InitWeights()
        {
            InitWeights(1); 
        }
        #endregion

        #region Properties
        public int InDimension
        {
            get { return (Input.Length); }
        }

        public int OutDimension
        {
            get { return (Output.Length); }
        }

        public int WeightsCount
        {
            get { return Weight.Length; }
        }

        public int NeuronNo
        {
            get 
            { 
                int no = 0; 
                for (int i=0; i<Neuron.GetLength(0); i++)
                    no += Neuron[i].GetLength(0); 
                return (no); 
            }
        }

        public int InternalStateDimension
        {
            get
            {
                return InDimension + NeuronNo * 2;
            }
        }
        #endregion

        #region Access
        public void GetWeights(ref Vector weights)
        {
            int no = WeightsCount; 
            if (weights==null || weights.Dimension!=no)
                weights = new Vector(no); 
            for (int i=0; i<no; i++)
                weights[i] = Weight[i].Value; 
        }

        public void SetWeights(Vector weights)
        {
            int no = WeightsCount; 
            if (weights==null || weights.Dimension!=no)
                throw new Exception("Unable to set these weights"); 
            for (int i=0; i<no; i++)
                Weight[i].Value = weights[i]; 
        }

        public void AddToWeights(Vector vect, double scalar)
        {
            int no = WeightsCount; 
            if (vect==null || vect.Dimension!=no)
                throw new Exception("Unable to add this vector to weights"); 
            for (int i=0; i<no; i++)
                Weight[i].Value += vect[i]*scalar; 
        }
        #endregion

        #region Protected calculations
        protected void SetInput(Vector arguments)
        {
            int in_dim = Input.GetLength(0); 
            if (arguments.Dimension != in_dim)
                throw (new System.Exception("Throw at Neural.MLPerceptron(Vector)")); 
            for (int i=0; i<in_dim; i++)
                Input[i].Value = (arguments[i]-InputAverage[i])/InputStddev[i]; 
        }

        protected void SetOutGradient(Vector out_gradient)
        {
            int out_dim = OutDimension;
            if (out_gradient.Dimension != out_dim)
                throw new Exception("Neural.MLPerceptron.SetOutGradient: wrong dimension"); 
            for (int i=0; i<out_dim; i++)
                dL_dOutput[i].Value = out_gradient[i];
        }

        protected void CalculateAhead()
        {
            for (int i=0; i<Neuron.GetLength(0); i++)
                for (int j=0; j<Neuron[i].GetLength(0); j++)
                    Neuron[i][j].CalculateAhead(); 
        }

        protected void BackPropagateGradient()
        {
            for (int i=Neuron.GetLength(0)-1; i>=0; i--)
                for (int j=0; j<Neuron[i].GetLength(0); j++)
                    Neuron[i][j].BackPropagateGradient(); 
        }
        
        protected void Approximate(Vector input, Vector output)
        {
            int out_dim = Output.GetLength(0); 
            SetInput(input); 
            CalculateAhead(); 
            for (int i=0; i<out_dim; i++)
                output[i] = Output[i].Value; 
        }

        //	preceding forward calculations required 
        protected void BackPropagateGradient(Vector out_gradient, Vector weight_gradient)
        {
            SetOutGradient(out_gradient); 
            BackPropagateGradient(); 
            int no = WeightsCount; 
            for (int i=0; i<no; i++)
                weight_gradient[i] = dL_dWeight[i].Value; 
        }
        #endregion 

        #region Public calculations
        public void Approximate(Vector input, ref Vector output)
        {
            int out_dim = Output.GetLength(0); 
            if (output == null || out_dim != output.Dimension)
                output = new Vector(out_dim); 
            Approximate(input, output); 
        }

        //	preceding forward calculations required 
        public void BackPropagateGradient(Vector out_gradient, ref Vector weight_gradient)
        {
            int no = WeightsCount; 
            if (weight_gradient==null || weight_gradient.Dimension!=no)
                weight_gradient = new Vector(no); 
            BackPropagateGradient(out_gradient, weight_gradient); 
        }

        //	dOutput_dInput[j,i] = dOutput[j]/dInput[i]
        //	preceding forward calculations required 
        public void Get_dOutput_dInput(ref Matrix doutput_dinput)
        {	
            int input;
            int output;
            int in_dim = Input.GetLength(0); 
            int out_dim = Output.GetLength(0); 

            if (doutput_dinput==null || doutput_dinput.Height!=out_dim || doutput_dinput.Width!=in_dim)
                doutput_dinput = new Matrix(out_dim, in_dim); 

            for (output=0; output<out_dim; output++)
                dL_dOutput[output].Value = 0; 
            for (output=0; output<out_dim; output++)
            {
                dL_dOutput[output].Value = 1; 
                BackPropagateGradient(); 

                for (input=0; input<in_dim; input++)
                {
                    doutput_dinput[output,input] = 0; 
                    for (int k=0; k<Neuron[0].GetLength(0); k++)
                        doutput_dinput[output,input] += Neuron[0][k].dL_dX.Value * Neuron[0][k].Weight[input].Value;
                    doutput_dinput[output,input] /= InputStddev[input]; 
                }

                dL_dOutput[output].Value = 0;
            }
        }

        //	dOutput_dWeight[j,i] = dOutput[j]/dWeight[i]
        //	preceding forward calculations required 
        public void Get_dOutput_dWeight(ref Matrix doutput_dweight)
        {	
            int j,i; 
            int out_dim = Output.GetLength(0); 
            int w_dim = Weight.Length;

            if (doutput_dweight==null || doutput_dweight.Height!=out_dim || doutput_dweight.Width!=w_dim)
                doutput_dweight = new Matrix(out_dim, w_dim); 

            for (j=0; j<out_dim; j++)
                dL_dOutput[j].Value = 0; 
            for (j=0; j<out_dim; j++)
            {
                dL_dOutput[j].Value = 1; 
                BackPropagateGradient(); 
                for (i=0; i<w_dim; i++)
                    doutput_dweight[j,i] = dL_dWeight[i].Value; 
                dL_dOutput[j].Value = 0; 
            }
        }
        #endregion
    }
}
